{{#packageName}}
package {{packageName}};
{{/packageName}}

import com.apzda.cloud.gsvc.client.IServiceCaller;
import com.apzda.cloud.gsvc.config.IServiceConfigure;
import com.apzda.cloud.gsvc.core.GatewayServiceRegistry;
import com.apzda.cloud.gsvc.exception.IExceptionHandler;
import com.apzda.cloud.gsvc.grpc.FluxObserver;
import com.apzda.cloud.gsvc.grpc.StubFactoryAdapter;
import com.apzda.cloud.gsvc.server.ConditionalOnLocalImpl;
import com.apzda.cloud.gsvc.server.IServiceMethodHandler;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;

import java.time.Duration;
import java.util.Optional;

{{#deprecated}}
@java.lang.Deprecated
{{/deprecated}}
@Configuration(proxyBeanMethods = false)
public class {{className}} {
    private static final Logger log = LoggerFactory.getLogger({{className}}.class);
    private static final java.util.Map<String, Object[]> METHOD_META_INFO = new java.util.HashMap<>();

    static {
    {{#unaryMethods}}
        METHOD_META_INFO.put("{{methodName}}",new Object[]{
            io.grpc.MethodDescriptor.MethodType.UNARY,
            {{inputType}}.class,
            {{outputType}}.class
        });
    {{/unaryMethods}}
    {{#serverStreamingMethods}}
        METHOD_META_INFO.put("{{methodName}}",new Object[]{
            io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING,
            {{inputType}}.class,
            {{outputType}}.class
        });
    {{/serverStreamingMethods}}
    {{#clientStreamingMethods}}
        METHOD_META_INFO.put("{{methodName}}",new Object[]{
            io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING,
            {{inputType}}.class,
            {{outputType}}.class
        });
    {{/clientStreamingMethods}}
    {{#biStreamingWithoutClientStreamMethods}}
        METHOD_META_INFO.put("{{methodName}}",new Object[]{
            io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING,
            {{inputType}}.class,
            {{outputType}}.class
        });
    {{/biStreamingWithoutClientStreamMethods}}
        GatewayServiceRegistry.register({{interfaceClassName}}.class, METHOD_META_INFO);
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "apzda.cloud.reference.{{interfaceClassName}}.grpc.enabled", havingValue = "false", matchIfMissing = true)
    {{interfaceClassName}} gsvc{{interfaceClassName}}Stub (IServiceCaller serviceCaller) {
        GatewayServiceRegistry.registerProxy({{interfaceClassName}}.class, "http");
        return new {{interfaceClassName}}() {
        {{#unaryMethods}}
            @Override
            public {{outputType}} {{methodName}}({{inputType}} request) {
                return serviceCaller.unaryCall({{interfaceClassName}}.class, "{{methodName}}", request, {{inputType}}.class, {{outputType}}.class);
            }
        {{/unaryMethods}}
        {{#serverStreamingMethods}}
            @Override
            public reactor.core.publisher.Flux<{{outputType}}> {{methodName}}({{inputType}} request) {
                return serviceCaller.serverStreamingCall({{interfaceClassName}}.class, "{{methodName}}", request, {{inputType}}.class, {{outputType}}.class);
            }
        {{/serverStreamingMethods}}
        {{#clientStreamingMethods}}
            @Override
            public reactor.core.publisher.Flux<{{outputType}}> {{methodName}}(reactor.core.publisher.Flux<{{inputType}}> request) {
                return serviceCaller.clientStreamingCall({{interfaceClassName}}.class, "{{methodName}}", request, {{inputType}}.class, {{outputType}}.class);
            }
        {{/clientStreamingMethods}}
        {{#biStreamingWithoutClientStreamMethods}}
            @Override
            public reactor.core.publisher.Flux<{{outputType}}> {{methodName}}(reactor.core.publisher.Flux<{{inputType}}> request) {
                return serviceCaller.bidiStreamingCall({{interfaceClassName}}.class, "{{methodName}}", request, {{inputType}}.class, {{outputType}}.class);
            }
        {{/biStreamingWithoutClientStreamMethods}}
            @Override
            public String toString() {
                return getClass().getName() + "@WebClientStub";
            }
        };
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "apzda.cloud.reference.{{interfaceClassName}}.grpc.enabled", havingValue = "true")
    {{interfaceClassName}} grpc{{interfaceClassName}}Stub (final StubFactoryAdapter adapter, final IServiceConfigure serviceConfigure) {
        GatewayServiceRegistry.registerProxy({{interfaceClassName}}.class, "grpc");
        final String svcName = serviceConfigure.getSvcName("{{interfaceClassName}}");
        final {{interfaceClassName}}Grpc.{{interfaceClassName}}BlockingStub blockingStub = adapter.createBlockingStub(svcName,
                {{interfaceClassName}}Grpc.{{interfaceClassName}}BlockingStub.class);
        final {{interfaceClassName}}Grpc.{{interfaceClassName}}Stub stub = adapter.createAsyncStub(svcName,
                {{interfaceClassName}}Grpc.{{interfaceClassName}}Stub.class);
        final Duration timeout = serviceConfigure.getReadTimeout("{{interfaceClassName}}", true);

        return new {{interfaceClassName}}() {
        {{#unaryMethods}}
            @Override
            public {{outputType}} {{methodName}}({{inputType}} request) {
                return blockingStub.{{methodName}}(request);
            }
        {{/unaryMethods}}
        {{#serverStreamingMethods}}
            @Override
            public reactor.core.publisher.Flux<{{outputType}}> {{methodName}}({{inputType}} request) {
                final FluxObserver<{{outputType}}> observer = new FluxObserver<>();
                stub.{{methodName}}(request, observer);
                return observer.asFlux().timeout(timeout);
            }
        {{/serverStreamingMethods}}
        {{#clientStreamingMethods}}
            @Override
            public reactor.core.publisher.Flux<{{outputType}}> {{methodName}}(reactor.core.publisher.Flux<{{inputType}}> request) {
                final FluxObserver<{{outputType}}> observer = new FluxObserver<>();
                final StreamObserver<{{inputType}}> reqObserver = stub.{{methodName}}(observer);

                request.doOnComplete(reqObserver::onCompleted)
                    .doOnError(reqObserver::onError)
                    .subscribeOn(reactor.core.scheduler.Schedulers.boundedElastic())
                    .subscribe(reqObserver::onNext);

                return observer.asFlux().timeout(timeout);
            }
        {{/clientStreamingMethods}}
        {{#biStreamingWithoutClientStreamMethods}}
            @Override
            public reactor.core.publisher.Flux<{{outputType}}> {{methodName}}(reactor.core.publisher.Flux<{{inputType}}> request) {
                final FluxObserver<{{outputType}}> resultObserver = new FluxObserver<>();
                final StreamObserver<{{inputType}}> reqObserver = stub.{{methodName}}(resultObserver);

                request.doOnComplete(reqObserver::onCompleted)
                    .doOnError(reqObserver::onError)
                    .subscribeOn(reactor.core.scheduler.Schedulers.boundedElastic())
                    .subscribe(reqObserver::onNext);

                return resultObserver.asFlux().timeout(timeout);
            }
        {{/biStreamingWithoutClientStreamMethods}}
            @Override
            public String toString() {
                return getClass().getName() + "@GrpcClientStub";
            }
        };
    }

    @com.apzda.cloud.gsvc.grpc.GrpcService
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "apzda.cloud.service.{{interfaceClassName}}.grpc.enabled", havingValue = "true")
    {{interfaceClassName}}Grpc.{{interfaceClassName}}ImplBase grpc{{interfaceClassName}}ImplAdapter(final {{interfaceClassName}} serviceImpl) {
        return new {{interfaceClassName}}Grpc.{{interfaceClassName}}ImplBase() {
        {{#unaryMethods}}
            @Override
            public void {{methodName}}({{inputType}} request, StreamObserver<{{outputType}}> responseObserver) {
                try {
                    responseObserver.onNext(serviceImpl.{{methodName}}(request));
                    responseObserver.onCompleted();
                }
                catch (Exception e) {
                    responseObserver.onError(e);
                    // throw e;
                }
            }
        {{/unaryMethods}}
        {{#serverStreamingMethods}}
            @Override
            public void {{methodName}}({{inputType}} request, StreamObserver<{{outputType}}> responseObserver) {
                try {
                    serviceImpl.{{methodName}}(request)
                        .doOnError(responseObserver::onError)
                        .doOnComplete(responseObserver::onCompleted)
                        .subscribeOn(reactor.core.scheduler.Schedulers.boundedElastic())
                        .subscribe(responseObserver::onNext);
                }
                catch (Exception e) {
                    responseObserver.onError(e);
                    // throw e;
                }
            }
        {{/serverStreamingMethods}}
        {{#clientStreamingMethods}}
            @Override
            public StreamObserver<{{inputType}}> {{methodName}}(StreamObserver<{{outputType}}> responseObserver) {
                final FluxObserver<{{inputType}}> req = new FluxObserver<>();
                final reactor.core.publisher.Flux<{{outputType}}> res = serviceImpl.{{methodName}}(req.asFlux());

                res.doOnComplete(responseObserver::onCompleted)
                    .doOnError(responseObserver::onError)
                    .subscribeOn(reactor.core.scheduler.Schedulers.boundedElastic())
                    .subscribe(responseObserver::onNext);

                return req;
            }
        {{/clientStreamingMethods}}
        {{#biStreamingWithoutClientStreamMethods}}
            @Override
            public StreamObserver<{{inputType}}> {{methodName}}(StreamObserver<{{outputType}}> responseObserver) {
                final FluxObserver<{{inputType}}> req = new FluxObserver<>();
                final reactor.core.publisher.Flux<{{outputType}}> res = serviceImpl.{{methodName}}(req.asFlux());

                res.doOnComplete(responseObserver::onCompleted)
                    .doOnError(responseObserver::onError)
                    .subscribeOn(reactor.core.scheduler.Schedulers.boundedElastic())
                    .subscribe(responseObserver::onNext);

                return req;
            }
        {{/biStreamingWithoutClientStreamMethods}}
        };
    }

    @Bean
    @ConditionalOnLocalImpl({{interfaceClassName}}.class)
    RouterFunction<ServerResponse> gsvc{{interfaceClassName}}RouterFunction(
        {{interfaceClassName}} serviceImpl,
        IServiceConfigure serviceConfigure, IServiceMethodHandler serviceMethodHandler,
        IExceptionHandler gsvcExceptionHandler) {
        RouterFunctions.Builder route = RouterFunctions.route();
        String cfgName = GatewayServiceRegistry.cfgName({{interfaceClassName}}.class);
        String serviceName = GatewayServiceRegistry.svcName({{interfaceClassName}}.class);
        String path = "/~" + serviceName + "/";
        if (GatewayServiceRegistry.getDeclaredServiceMethods({{interfaceClassName}}.class).isEmpty()) {
            route.path(path, () -> (request) -> Optional.empty());
        }
        else {
    {{#unaryMethods}}
        log.trace("EW Route {}{} to {}.{}", path, "{{methodName}}", serviceName, "{{methodName}}");
        route.POST(path+"{{methodName}}",
        (request) -> serviceMethodHandler.handleUnary(request, {{interfaceClassName}}.class, "{{methodName}}", (param) -> {
            return serviceImpl.{{methodName}}(({{inputType}}) param);
        }));
    {{/unaryMethods}}
    {{#serverStreamingMethods}}
        log.trace("EW Route {}{} to {}.{}", path, "{{methodName}}", serviceName, "{{methodName}}");
        route.POST(path+"{{methodName}}",
        (request) -> serviceMethodHandler.handleServerStreaming(request, {{interfaceClassName}}.class, "{{methodName}}", (param) -> {
            return serviceImpl.{{methodName}}(({{inputType}}) param);
        }));
    {{/serverStreamingMethods}}
    {{#clientStreamingMethods}}
        log.trace("EW Route {}{} to {}.{}", path, "{{methodName}}", serviceName, "{{methodName}}");
        route.POST(path+"{{methodName}}",
        (request) -> serviceMethodHandler.handleBidStreaming(request, {{interfaceClassName}}.class, "{{methodName}}", (param) -> {
            return serviceImpl.{{methodName}}((reactor.core.publisher.Flux<{{inputType}}>) param);
        }));
    {{/clientStreamingMethods}}
    {{#biStreamingWithoutClientStreamMethods}}
        log.trace("EW Route {}{} to {}.{}", path, "{{methodName}}", serviceName, "{{methodName}}");
        route.POST(path+"{{methodName}}",
        (request) -> serviceMethodHandler.handleBidStreaming(request, {{interfaceClassName}}.class, "{{methodName}}", (param) -> {
            return serviceImpl.{{methodName}}((reactor.core.publisher.Flux<{{inputType}}>) param);
        }));
    {{/biStreamingWithoutClientStreamMethods}}
        }

        return route.onError(Exception.class, gsvcExceptionHandler::handle).build();
    }
}
